/*
 * @(#)${NAME}.java
 *
 * Copyright 2002 - 2005 JIDE Software Inc. All rights reserved.
 */
package com.element.swing.resize;

import com.element.plaf.UIDefaultsLookup;

import javax.swing.*;
import javax.swing.event.MouseInputListener;
import java.awt.*;

/**
 * <code>Resizable</code> is a class that supports the resizable feature.
 * <p/>
 * To use it and make a component resizable, you just need to create new Resizable(component) and pass in that component
 * to the constructor. <code>Resizable</code> uses {@link #beginResizing(int)}, {@link #resizing(int, int, int, int,
 * int)} and {@link #endResizing(int)} to archive the resizing effect. It should work for most cases. However if it
 * doesn't work as expected for some layouts, you can override one of all of the three methods to make it working.
 */
public class Resizable {
	public static final int NONE = 0x0;
	public static final int UPPER_LEFT = 0x1;
	public static final int UPPER = 0x2;
	public static final int UPPER_RIGHT = 0x4;
	public static final int RIGHT = 0x8;
	public static final int LOWER_RIGHT = 0x10;
	public static final int LOWER = 0x20;
	public static final int LOWER_LEFT = 0x40;
	public static final int LEFT = 0x80;
	public static final int ALL = 0xFF;

	private int _resizableCorners = 0xFF;

	private int _resizeCornerSize = 16;

	public static final String PROPERTY_RESIZABLE_CORNERS = "resizableCorner";
	public static final String PROPERTY_RESIZE_CORNER_SIZE = "resizeCornerSize";

	protected final JComponent _component;
	private Insets _resizeInsets;
	private MouseInputListener _mouseInputAdapter;
	private int _snapGridSize = 1;

	private boolean _topLevel;

	/**
	 * Creates a new <code>Resizable</code>. This call will make the component to be resizable.
	 *
	 * @param component the component that should be resizable.
	 */
	public Resizable(JComponent component) {
		_component = component;
		installListeners();
	}

	/**
	 * Gets the resizable corners. The value is a bitwise OR of eight constants defined in {@link Resizable}.
	 *
	 * @return resizable corners.
	 */
	public int getResizableCorners() {
		return _resizableCorners;
	}

	/**
	 * Sets resizable corners.
	 *
	 * @param resizableCorners new resizable corners. The value is a bitwise OR of eight constants defined in {@link
	 *                         Resizable}.
	 */
	public void setResizableCorners(int resizableCorners) {
		if (_resizableCorners != resizableCorners) {
			int old = _resizableCorners;
			_resizableCorners = resizableCorners;
			_component.firePropertyChange(PROPERTY_RESIZABLE_CORNERS, old, _resizableCorners);
		}
	}

	/**
	 * Gets resize corner size. This size is the corner's sensitive area which will trigger the resizing from both
	 * sides.
	 *
	 * @return the resize corner size.
	 */
	public int getResizeCornerSize() {
		return _resizeCornerSize;
	}

	/**
	 * Sets the resize corner size.
	 *
	 * @param resizeCornerSize the resize corner size.
	 */
	public void setResizeCornerSize(int resizeCornerSize) {
		if (_resizeCornerSize != resizeCornerSize) {
			int old = _resizeCornerSize;
			_resizeCornerSize = resizeCornerSize;
			_component.firePropertyChange(PROPERTY_RESIZE_CORNER_SIZE, old, _resizeCornerSize);
		}
	}

	/**
	 * Installs the listeners needed to perform resizing operations. You do not need to call this method directly.
	 * Constructor will call this method automatically.
	 */
	protected void installListeners() {
		_mouseInputAdapter = createMouseInputListener();
		_component.addMouseListener(_mouseInputAdapter);
		_component.addMouseMotionListener(_mouseInputAdapter);
	}

	/**
	 * Uninstalls the listeners that created to perform resizing operations. After the uninstallation, the component
	 * will not be resizable anymore.
	 */
	public void uninstallListeners() {
		_component.removeMouseListener(_mouseInputAdapter);
		_component.removeMouseMotionListener(_mouseInputAdapter);
		_mouseInputAdapter = null;
	}

	/**
	 * Creates the MouseInputListener for resizing. Subclass can override this method to provide its own
	 * MouseInputListener to customize existing one.
	 *
	 * @return the MouseInputListener for resizing.
	 */
	protected MouseInputListener createMouseInputListener() {
		return new ResizableMouseInputAdapter(this);
	}

	/**
	 * Gets the mouse adapter for resizing.
	 *
	 * @return the mouse adapter for resizing.
	 */
	public MouseInputListener getMouseInputAdapter() {
		return _mouseInputAdapter;
	}

	/**
	 * This method is called when resizing operation started.
	 *
	 * @param resizeCorner the resize corner.
	 */
	public void beginResizing(int resizeCorner) {
	}

	/**
	 * This method is called during the resizing of ResizablePanel. In default implementation, it call
	 * <code><pre>
	 * setPreferredSize(new Dimension(newW, newH));
	 * getParent().doLayout();
	 * </pre></code>
	 * in fact, depending on where you added this ResizablePanel, you may need to override this method to do something
	 * else. For example, {@link ResizableWindow} uses <code>ResizablePanel</code> to implement resizable feature in
	 * JWindow. It overrides this method to call setBounds on JWindow itself.
	 *
	 * @param resizeCorner the resize corner.
	 * @param newX         the new x position.
	 * @param newY         the new y position.
	 * @param newW         the new width.
	 * @param newH         the new height.
	 */
	public void resizing(int resizeCorner, int newX, int newY, int newW, int newH) {
		Dimension minimumSize = _component.getMinimumSize();
		Dimension maximumSize = _component.getMaximumSize();
		if (newW < minimumSize.width) {
			newW = minimumSize.width;
		}
		if (newH < minimumSize.height) {
			newW = minimumSize.height;
		}
		if (newW > maximumSize.width) {
			newW = maximumSize.width;
		}
		if (newH > maximumSize.height) {
			newH = maximumSize.height;
		}
		_component.setPreferredSize(new Dimension(newW, newH));
		_component.getParent().doLayout();
	}

	/**
	 * The method is called when resizing ends.
	 *
	 * @param resizeCorner the resize corner.
	 */
	public void endResizing(int resizeCorner) {
	}

	/**
	 * Checks if the Resizable is added to a top level component. <p> If it's top level component, it will use screen
	 * coordinates to do all calculations during resizing. If resizing the resizable panel won't affect any top level
	 * container's position, you can return false here. Otherwise, return true. The default implementation always return
	 * false. Subclasses can override to return different value. In the case of ResizableWindow or ResizableDialog, this
	 * method is overridden and returns true.
	 *
	 * @return false.
	 */
	public boolean isTopLevel() {
		return _topLevel;
	}

	/**
	 * To indicates this <code>Resizable</code> is installed on a top level component such as JWindow, JDialog and
	 * JFrame v.s. a JPanel which is not a top level component because a JPanel must be added to another top level
	 * component in order to be displayed.
	 *
	 * @param topLevel true or false.
	 */
	public void setTopLevel(boolean topLevel) {
		_topLevel = topLevel;
	}

	/**
	 * Gets the component which has this Resizable object.
	 *
	 * @return the component which has this Resizable object.
	 */
	public JComponent getComponent() {
		return _component;
	}

	/**
	 * Returns the insets that should be used to calculate the resize area. Unless you have used setResizeInsets or
	 * overridden this method, it'll return the insets of the component.
	 *
	 * @return the insets that should be used to calculate the resize area.
	 */
	public Insets getResizeInsets() {
		if (_resizeInsets != null) {
			return _resizeInsets;
		}
		return getComponent().getInsets();
	}

	/**
	 * Sets the insets the be used to calculate the resize area.
	 *
	 * @param resizeInsets
	 */
	public void setResizeInsets(Insets resizeInsets) {
		_resizeInsets = resizeInsets;
	}

	/**
	 * Gets the snap grid size.
	 *
	 * @return the snap grid size.
	 * @see #setSnapGridSize(int)
	 */
	public int getSnapGridSize() {
		return _snapGridSize;
	}


	/**
	 * Sets the snap grid size.
	 * <p/>
	 * Snap grid size is used to make resizing easier.
	 * <p/>
	 * By default, the size is 1 pixel.
	 *
	 * @param snapGridSize the grid size
	 */
	public void setSnapGridSize(int snapGridSize) {
		_snapGridSize = snapGridSize;
	}

	public static class ResizeCorner extends JComponent {
		static final int SIZE = 16;
		private int _corner = LOWER_RIGHT;

		public ResizeCorner() {
		}

		public ResizeCorner(int corner) {
			_corner = corner;
		}

		public int getCorner() {
			return _corner;
		}

		public void setCorner(int corner) {
			_corner = corner;
		}

		@Override
		public Dimension getPreferredSize() {
			return new Dimension(SIZE, SIZE);
		}

		@Override
		protected void paintComponent(Graphics g) {
			super.paintComponent(g);
			int size = Math.min(getWidth(), getHeight());
			int count = Math.min(size / 4, 4);
			Color old = g.getColor();
			int corner = getCorner();
			boolean ltr = getComponentOrientation().isLeftToRight();
			switch (corner) {
				case LOWER_RIGHT -> {
					g.setColor(UIDefaultsLookup.getColor("controlLtHighlight"));
					int delta = 0;
					for (int i = 0; i < count; i++) {
						delta += 4;
						if (ltr) {
							g.drawLine(size, size - delta, size - delta, size);
						} else {
							g.drawLine(0, delta, size - delta, size);
						}
					}
					g.setColor(UIDefaultsLookup.getColor("controlShadow"));
					delta = 0;
					for (int i = 0; i < count; i++) {
						delta += 4;
						if (ltr) {
							g.drawLine(size, size - delta + 1, size - delta + 1, size);
							g.drawLine(size, size - delta + 2, size - delta + 2, size);
						} else {
							g.drawLine(0, delta + 1, size - delta - 1, size);
							g.drawLine(0, delta + 2, size - delta - 2, size);
						}
					}
				}
				case UPPER_RIGHT -> {
					g.setColor(UIDefaultsLookup.getColor("controlLtHighlight"));
					int delta = 0;
					for (int i = 0; i < count; i++) {
						delta += 4;
						if (ltr) {
							g.drawLine(size - delta, 0, size, delta);
						} else {
							g.drawLine(delta, 0, size, size - delta);
						}
					}
					g.setColor(UIDefaultsLookup.getColor("controlShadow"));
					delta = 0;
					for (int i = 0; i < count; i++) {
						delta += 4;
						if (ltr) {
							g.drawLine(size - delta + 1, 0, size, delta - 1);
							g.drawLine(size - delta + 2, 0, size, delta - 2);
						} else {
							g.drawLine(delta + 1, 0, size, size - delta - 1);
							g.drawLine(delta + 2, 0, size, size - delta - 2);
						}
					}
				}
			}
			g.setColor(old);
		}
	}
}
